﻿using DZParallelLib.Data;
using DZParallelLib.Utility;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using System.Xml.Serialization;
using VerifyArgs;

namespace DZParallelLib
{
    /// <summary>
    /// Given a list of the deepzoom images that make up this collection, generate the deepzoom collection
    /// xml file that can then be used by the RenderDCCollectionImage as input. The files are used as input, and the
    /// names are also used to index into a database for previously rendered items.
    /// </summary>
    public class RenderDZCollectionFromImages
    {
        /// <summary>
        /// Code that will do the rendering for the tile image.
        /// </summary>
        private RenderDZCollectionImage _imageRender = new RenderDZCollectionImage() { NumberOfLevels = 9, TileSize = 256 };

        /// <summary>
        /// XML loader for each file
        /// </summary>
        private XmlSerializer _imageLoader = new XmlSerializer(typeof(Image));

        /// <summary>
        /// XML loader for the full collection.
        /// </summary>
        private XmlSerializer _collectionLoader = new XmlSerializer(typeof(Collection));

        /// <summary>
        /// Given a set of sub-images, render the collection. Only add new images that are needed. Return a index number for each item
        /// in the collection.
        /// </summary>
        /// <param name="inputDZImageXMLFiles">Pointer to all the files that contain the sub-image information</param>
        /// <param name="convertDZFileToURL">A Converter to change from the full image path to a URL path that can be used by the web server</param>
        /// <param name="outputFile">The name of the output collection file. The image files and sub-images will be written in this directory as well.</param>
        /// <returns>An ordered list of image IDs in the collection, each entry matches with the same index in teh input xml files</returns>
        public async Task<IEnumerable<ulong>> RenderCollection(IEnumerable<FileInfo> inputDZImageXMLFiles, Func<FileInfo, string> convertDZFileToURL, FileInfo outputFile)
        {
            Verify.Args(new { inputDZImageXMLFiles }).NotNull().AllFilesExist();
            Verify.Args(new { outputFile }).NotNull();
            Verify.Args(new { counvertDZFileToURL = convertDZFileToURL }).NotNull();

            //
            // First, write out the master DZ file
            //

            var updated = await WriteNewCollectionFile(inputDZImageXMLFiles, convertDZFileToURL, outputFile);

            //
            // Next, run the render for only the images that are needed.
            //

            await _imageRender.RenderImage(outputFile, new DirectoryInfo(Path.Combine(outputFile.Directory.FullName, string.Format("{0}_files", Path.GetFileNameWithoutExtension(outputFile.Name)))), new HashSet<ulong>(updated));

            return GetImageIDs(inputDZImageXMLFiles, convertDZFileToURL, outputFile.Directory);
        }

        /// <summary>
        /// Return the image id's for images that already exist. Fail if we can't find an image!
        /// </summary>
        /// <param name="inputDZImageXMLFiles"></param>
        /// <param name="convertDZFileToURL"></param>
        /// <returns></returns>
        private IEnumerable<ulong> GetImageIDs(IEnumerable<FileInfo> inputDZImageXMLFiles, Func<FileInfo, string> convertDZFileToURL, DirectoryInfo db)
        {
            var preRenderedImages = LoadRenderedImagesDB(db).ToDictionary(i => i._url, i => i._id);

            return (from i in inputDZImageXMLFiles
                    select preRenderedImages[convertDZFileToURL(i)]).ToArray();
        }

        /// <summary>
        /// Given a set of input image XML files, generate a new collection file in the specified place. Return
        /// a list of images ID's that have been re-rendered since the last update came through here.
        /// </summary>
        /// <param name="inputDZImageXMLFiles">A list of the XML files that contain each image</param>
        /// <param name="convertDZFileToURL">A function that takes the FileInfo and converts it to a relative path that can be used by the PV control to find this XML files.</param>
        /// <param name="outputFile">Where the collection xml file will be written</param>
        /// <returns>A list of the image ID's that need to be re-rendered</returns>
        public async Task<IEnumerable<ulong>> WriteNewCollectionFile(IEnumerable<FileInfo> inputDZImageXMLFiles,
            Func<FileInfo, string> convertDZFileToURL,
            FileInfo outputFile)
        {
            Verify.Args(new { inputDZImageXMLFiles }).NotNull().AllFilesExist();
            Verify.Args(new { outputFile }).NotNull();
            Verify.Args(new { convertDZFileToURL }).NotNull();

            //
            // Create the output image file and save it.
            //

            var c = new Collection();
            c.MaxLevel = 8;
            c.TileSize = 256;
            c.Format = "png";

            //
            // Load in the old image database
            //

            var preRenderedImages = LoadRenderedImagesDB(outputFile.Directory);
            long preRenderedMaxID = preRenderedImages.Length > 0 ? (long)preRenderedImages.Select(i => i._id).Max() : -1;
            var preRenderedImageDict = preRenderedImages.ToDictionary(i => i._url, i => i._id);

            //
            // Split the images we are looking at into two sets - one if we already have seen them, and the other
            // if not.
            //

            var splitImages = from f in inputDZImageXMLFiles
                              let url = convertDZFileToURL(f)
                              group f by preRenderedImageDict.ContainsKey(url);

            //
            // The old guys are pretty easy. We need to replicate every single one that was in there (sadly, this is the way of the
            // PV controller - everything has to be in there).
            //

            var oldImages = from i in preRenderedImages
                            select ConvertToCollectionImage(i._id, i._size, i._url);

            //
            // For the new ones, assign new ID's to them.
            //

            var newImageFiles = from g in splitImages
                                where !g.Key
                                from f in g
                                select f;

            var newImages = from f in newImageFiles.Zip(MoreLinq.MoreEnumerable.Generate(preRenderedMaxID + 1, i => i + 1), (f, count) => Tuple.Create(count, f))
                            let image = LoadDZImageFile(f.Item2)
                            let url = convertDZFileToURL(f.Item2)
                            select ConvertToCollectionImage((ulong)f.Item1, image.Size, url);

            //
            // And put them together into the full array.
            //

            c.Items = newImages.Concat(oldImages).OrderBy(i => i.Id).ToArray();
            c.NextItemId = c.Items.Length > 0 ? c.Items.Select(im => im.Id).Max() + 1 : 0;

            //
            // Finally, write out the full image database!
            //

            var newImageDB = c.Items.Select(i => new RenderedImageInfo() { _id = i.Id, _url = i.Source, _size = i.Size }).ToArray();
            SaveRenderedImagesDB(outputFile.Directory, newImageDB);

            //
            // Next, write it out!
            //

            using (var w = outputFile.CreateText())
            {
                _collectionLoader.Serialize(w, c);
                w.Close();
            }
            outputFile.Refresh();

            return newImages.Select(i => i.Id).ToArray();
        }

        /// <summary>
        /// The info for a rendered image
        /// </summary>
        public struct RenderedImageInfo
        {
            public string _url;
            public ulong _id;
            public uint32size _size;
        }

        /// <summary>
        /// Load up and save the image database
        /// </summary>
        private XmlSerializer _renderedImageInfoLoader = new XmlSerializer(typeof(RenderedImageInfo[]));

        /// <summary>
        /// Load the image database for the images we've already rendered.
        /// </summary>
        /// <param name="directoryInfo"></param>
        /// <returns></returns>
        private RenderedImageInfo[] LoadRenderedImagesDB(DirectoryInfo directoryInfo)
        {
            var df = RenderedImageDBFile(directoryInfo);
            if (!df.Exists)
                return new RenderedImageInfo[0];

            using (var r = df.OpenText())
            {
                return _renderedImageInfoLoader.Deserialize(r) as RenderedImageInfo[];
            }
        }

        /// <summary>
        /// Save the rendered database
        /// </summary>
        /// <param name="directoryInfo"></param>
        /// <param name="newImageDB"></param>
        private void SaveRenderedImagesDB(DirectoryInfo directoryInfo, RenderedImageInfo[] newImageDB)
        {
            var df = RenderedImageDBFile(directoryInfo);
            using (var w = df.CreateText())
            {
                _renderedImageInfoLoader.Serialize(w, newImageDB);
                w.Close();
            }
        }

        /// <summary>
        /// The rendered image database file
        /// </summary>
        /// <param name="directoryInfo"></param>
        /// <returns></returns>
        private FileInfo RenderedImageDBFile(DirectoryInfo directoryInfo)
        {
            return new FileInfo(Path.Combine(directoryInfo.FullName, "DZRenderedImageDbXML.rendered_info"));
        }

        /// <summary>
        /// Convert an image with a index into the output file.
        /// </summary>
        /// <param name="info"></param>
        /// <returns></returns>
        CollectionI ConvertToCollectionImage(ulong id, uint32size imageSize, string url)
        {
            var c = new CollectionI();
            c.Id = (uint)id;
            c.N = (uint)id;
            c.Source = url;
            c.Size = imageSize;
            return c;
        }

        /// <summary>
        /// Load a single image file into memory.
        /// </summary>
        /// <param name="f">Pointer to an xml file that already exists</param>
        /// <returns></returns>
        private Image LoadDZImageFile(FileInfo f)
        {
            using (var rdr = f.OpenText())
            {
                return _imageLoader.Deserialize(rdr) as Image;
            }
        }

        /// <summary>
        /// Remove the pre-existing image db - mostly to help with testing.
        /// </summary>
        /// <param name="directoryInfo"></param>
        internal void DeleteImageDB(DirectoryInfo directoryInfo)
        {
            var df = RenderedImageDBFile(directoryInfo);
            if (df.Exists)
                df.Delete();
        }
    }
}
